/* Copyright (C) 2007  Felipe A. Lessa e Luciano H. O. Santos
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 */
package br.unb.cic.algostep.gui.variaveis;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;

import javax.swing.JPanel;
import javax.swing.JScrollPane;

import br.unb.cic.algostep.IEstado;
import br.unb.cic.algostep.gui.Constantes;

/**
 * 
 * Mostra um conjunto de variáveis.
 * 
 * @author felipe.lessa
 *
 */
@SuppressWarnings("serial")
public class Mostrador extends JScrollPane implements IEstado {
	private static final Comparator<Color> comparadorColor = 
		new Comparator<Color>() {
			public int compare(Color c1, Color c2) {
				return new Integer(c1.getRGB()).compareTo(c2.getRGB());
			}
		};
	
	private static final Comparator<Variavel> comparadorVariavel = 
		new Comparator<Variavel>() {
			public int compare(Variavel v1, Variavel v2) {
				return v1.getName().compareToIgnoreCase(v2.getName());
			}
		};
	
	
	private double prop = -100.0;
	private Point ultimoPonto;
	private Variaveis vars = new Variaveis();
	private JPanel painelPrincipal;
	private Painel painelVars = null;
	private Function function = null;
	
	/**
	 * Cria um novo mostrador vazio.
	 */
	public Mostrador() {
		super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 
		      JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		
		painelPrincipal = new JPanel();
		painelPrincipal.setLayout(new GridLayout(2, 1));
		this.viewport.setView(painelPrincipal);
	}
	
	/**
	 * A proporção atual de todas as janelas.
	 * @return  a proporção (1.0 = 100%).
	 */
	public double getProporcao() {
		return prop;
	}
	
	/**
	 * Define a proporção a ser usada em todas as janelas.
	 * @param nova_prop
	 */
	public void setProporcao(double nova_prop) {
		if (prop == nova_prop)
			return;
		
 		double mult = nova_prop / prop;
		Rectangle r1 = viewport.getViewRect();
		double x = r1.getCenterX(), 
		       y = r1.getCenterY();
		
		if (painelVars != null)
			painelVars.setProporcao(nova_prop);
		prop = nova_prop;
		this.validate();
		
		Rectangle r2 = viewport.getViewRect();
		double x2 = x*mult - r2.width /2, 
		       y2 = y*mult - r2.height/2;
		Point p = new Point((int) Math.round(x2), (int) Math.round(y2));
		viewport.setViewPosition(p);
		
		if (function != null) {
			function.setProporcao(nova_prop);
		}
	}
	
	@Override
	public Insets getInsets() {
		// Força a proporção inicial
		if (prop <= 0)
			setProporcao(1.0);
		return super.getInsets();
	}
	
	/**
	 * Adiciona uma variável ao mostrador.
	 * @param v  variável a ser adicionada.
	 */
	public void adicionarVariavel(Variavel v) {
		if (function != null) {
			function.adicionarVariavel(v);
		} else {
			vars.adicionarVariavel(v);
		}
	}
	
	/**
	 * Remove uma variável do mostrador.
	 * @param nome  nome da variável a ser removida.
	 */
	public void removerVariavel(String nome) {
		vars.removerVariavel(nome);
	}
	
	/**
	 * Modifica o valor de uma variável.
	 * @param nome   nome da variável a ser modificada.
	 * @param valor  seu novo valor.
	 */
	public boolean modificarVariavel(String nome, Object valor) {
		if (function != null) {
			if (function.modificarVariavel(nome, valor))
				return true;
		}
		
		if (hasVariavel(nome)) {
			vars.modificarVariavel(nome, valor);
			return true;
		}
		
		return false;
	}

	/**
	 * Retorna uma cópia de uma variável.
	 * 
	 * @param nome
	 * 
	 * @return
	 */
	public Variavel getCopiaVariavel(String nome) {
		return vars.getCopiaVariavel(nome);
	}
	
	public Object getEstado() {
		return new EstadoMostrador(
				new Painel(vars),
				function);
	}

	public void setEstado(Object estado) {
		Rectangle r1 = viewport.getViewRect();
		
		painelPrincipal.removeAll();
		
		if (estado != null) {
			EstadoMostrador e = (EstadoMostrador) estado;
			painelVars = e.painel;
			function = e.function;
			
			painelPrincipal.add(painelVars);
			painelVars.criar();
			painelVars.setProporcao(prop < 0 ? 1.0 : prop);
			
			if (function != null) {
				function.setEstado(e.estadoFunction);
				painelPrincipal.add(function);
			}
			
			this.validate();
			this.repaint();
			
			Rectangle r2 = viewport.getViewRect();
			double x = r1.getCenterX() - r2.width/2,
			       y = r1.getCenterY() - r2.height/2;
			Point p = new Point((int) Math.round(x), (int) Math.round(y));
			viewport.setViewPosition(p);
		}
	}

	
	
	
	
	@Override
	public void processMouseEvent(MouseEvent e) {
		try {
			if (e.getID() != MouseEvent.MOUSE_PRESSED)
			{
				return;
			}
			ultimoPonto = e.getPoint();
		} finally {
			super.processMouseEvent(e);
		}
	}

	@Override
	public void processMouseMotionEvent(MouseEvent e) {
		try {
			if (painelVars == null || e.getID() != MouseEvent.MOUSE_DRAGGED)
			{
				return;
			}
			
			int diffX = e.getX() - ultimoPonto.x;
			int diffY = e.getY() - ultimoPonto.y;
			Dimension s = viewport.getExtentSize();
			Dimension m = painelVars.getSize();
			Point p = viewport.getViewPosition();
			double mult = -1.0; //Math.sqrt(prop);
			p.x += diffX * mult;
			p.y += diffY * mult;
			if (p.x < 0) p.x = 0;
			if (p.y < 0) p.y = 0;
			if (p.x + s.width > m.width) p.x = m.width - s.width;
			if (p.y + s.height > m.height) p.y = m.height - s.height;
			
			viewport.setViewPosition(p);
			
			ultimoPonto = e.getPoint();
		} finally {
			super.processMouseMotionEvent(e);
		}
	}
	
	public void startFunction(Function f) {
		if (function != null) {
			function.startFunction(f);
		} else {
			function = f;
		}
	}
	
	public boolean endFunction() {
		if (function != null) {
			if (!function.endFunction()) {
				function = null;
			}
			
			return true;
		}
		
		return false;
	}
	
	public boolean hasVariavel(String nome) {
		return vars.contains(nome);
	}
	
	
	/**
	 * Contém as variáveis *sem* mostrá-las.
	 */
	protected class Variaveis {
		private HashMap<String, Variavel> variaveis;

		/**
		 * Constrói um contâiner vazio.
		 */
		public Variaveis() {
			this.variaveis = new HashMap<String, Variavel>();
		}
		
		/**
		 * Adiciona uma variável ao contêiner.
		 * @param v  variável a ser adicionada.
		 */		
		public void adicionarVariavel(Variavel v) {
			variaveis.put(v.getName(), v);
		}
		
		/**
		 * Remove uma variável do contêiner.
		 * @param nome  nome da variável a ser removida.
		 */
		public void removerVariavel(String nome) {
			variaveis.remove(nome);
		}
		
		/**
		 * Modifica o valor de uma variável no contêiner.
		 * @param nome   nome da variável a ser modificada.
		 * @param valor  seu novo valor.
		 */
		public void modificarVariavel(String nome, Object valor) {
			Variavel v = variaveis.get(nome);
			if (v == null)
				throw new RuntimeException("Variável " + nome + " não existe.");
			v.setValor(valor);
		}
		
		/**
		 * Retorna uma cópia de uma variável.
		 * 
		 * @param nome
		 * @return
		 */
		public Variavel getCopiaVariavel(String nome) {
			Variavel v = variaveis.get(nome);
			if (v == null)
				throw new RuntimeException("Variável " + nome + " não existe.");
			return v.criarCopia();
		}
		
		/**
		 * Retorna uma cópia das variáveis como um HashMap separado
		 * por cores.
		 */
		public HashMap<Color, ArrayList<Variavel>> criarCopia() {
			HashMap<Color, ArrayList<Variavel>> ret = new
				HashMap<Color, ArrayList<Variavel>>();
			for (Variavel v : variaveis.values()) {
				Color cor = v.getCorTitulo();
				Variavel copia = v.criarCopia();
				if (ret.containsKey(cor)) {
					ret.get(cor).add(copia);
				} else {
					ArrayList<Variavel> novo = new ArrayList<Variavel>();
					novo.add(copia);
					ret.put(cor, novo);
				}
			}
			return ret;
		}
		
		public boolean contains(String nome) {
			return variaveis.containsKey(nome);
		}
	}
	
	/**
	 * Painel que mostra variáveis *estaticamente*, i.e. o conteúdo 
	 * delas *não* deve ser alterado. Este é o estado! =)
	 */
	protected class Painel extends JPanel {
		private HashMap<Color, ArrayList<Variavel>> map;
		private ArrayList<Janela> janelas = new ArrayList<Janela>();



		/**
		 * Cria um painel. 
		 * @see #criar()
		 */
		public Painel(Variaveis v) {
			super();
			assert (v != null);
			this.map = v.criarCopia();
			setLayout(new GridBagLayout());
		}
		
		/**
		 * Cria os painéis interiores. Chame este método *após* adicionar
		 * este painel a outro componente.
		 */
		public void criar() {
			if (map == null) return;
			Color[] cores = map.keySet().toArray(new Color[] {}); 
			Arrays.sort(cores, comparadorColor);
			GridBagConstraints c = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
					GridBagConstraints.FIRST_LINE_START,
					GridBagConstraints.NONE, 
					new Insets(0, 0, 0, 0), 0, 0);
			for (Color cor : cores) {
				Variavel[] vars = map.get(cor).toArray(new Variavel[] {});
				Arrays.sort(vars, comparadorVariavel);

				JPanel painel = new JPanel();
				c.gridy++;
				this.add(painel, c);
				
				int tamanho = 0;
				for (Variavel v : vars) {
					Janela j = new Janela(v);
					janelas.add(j);
					painel.add(j);
					j.validate();
					int este = j.getSize().width;
					if (tamanho <= 0)
						tamanho = este;
					else {
						tamanho += este;
						if (tamanho > Constantes.LARGURA_MAXIMA) {
							painel.remove(j);
							painel = new JPanel();
							c.gridy++;
							this.add(painel, c);
							painel.add(j);
							tamanho = este;
						}
					}
				}
			}
			map = null;
		}

		/**
		 * Define a proporção de todas as janelas contidos neste painel.
		 * @param prop   nova propoprção.
		 */
		public void setProporcao(double prop) {
			for (Janela j : janelas)
				j.setProporcao(prop);
			this.validate();
			this.validate();
			this.validate();
		}
	}
	
	
	private class EstadoMostrador {
		public Painel painel;
		public Function function;
		public Object estadoFunction = null;
		
		public EstadoMostrador(Painel painel, Function function) {
			this.painel = painel;
			this.function = function;
			
			if (function != null)
				this.estadoFunction = function.getEstado();
		}
	}
}
